<html>
  <head>
   <meta charset="utf-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <meta name="theme-color" content="#008000"/>
   <title>hello_ml</title>
   <link rel="manifest" href="hello_ml_manifest.json">
   <script>
    if('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/hello_ml_sw.js')
        .then(function() {
          console.log('Service Worker Registered');
        });
    }
   </script>
    <style>
      body {
        margin: 0;
      }
    </style>
  </head>
  <body>
  <h1>hello_ml</h1>
  <script src="three.js"></script>
  <script src="GLTFLoader.js"></script>
  <script>
    let container, scene, camera, session, model, controllerMeshes, eyeMesh;
    let mesher = null, planeTracker = null, handTracker = null, eyeTracker = null;

    const controllerGeometry = new THREE.BoxBufferGeometry(0.1, 0.2, 0.01);
    const controllerMaterial = new THREE.MeshPhongMaterial({
      color: 0x4caf50,
    });
    const _makeControllerMesh = (x = 0, y = 0, z = 0, qx = 0, qy = 0, qz = 0, qw = 1) => {
      const mesh = new THREE.Mesh(controllerGeometry, controllerMaterial);
      mesh.position.set(x, y, z);
      mesh.quaternion.set(qx, qy, qz, qw);
      // mesh.matrix.compose(mesh.position, mesh.quaternion, mesh.scale);
      mesh.updateMatrix();
      mesh.updateMatrixWorld();
      mesh.matrixAutoUpdate = false;
      mesh.frustumCulled = false;
      return mesh;
    };
    const _getControllerIndex = inputSource => inputSource.handedness === 'left' ? 0 : 1;

    function init() {
      container = document.createElement('div');
      document.body.appendChild(container);

      scene = new THREE.Scene();
      scene.matrixAutoUpdate = false;
      // scene.background = new THREE.Color(0x3B3961);

      camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
      // camera.position.set(0, 1, 0);
      camera.lookAt(new THREE.Vector3());
      scene.add(camera);

      const ambientLight = new THREE.AmbientLight(0x808080);
      scene.add(ambientLight);

      const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 1);
      directionalLight.position.set(1, 1, 1);
      scene.add(directionalLight);

      controllerMeshes = [
        _makeControllerMesh(-0.1),
        _makeControllerMesh(0.1),
      ];
      controllerMeshes.forEach(controllerMesh => {
        scene.add(controllerMesh);
      });

      {
        const loader = new THREE.GLTFLoader(); // .setPath( 'models/' );
        loader.load( 'exobot.glb', function ( o ) {

          o = o.scene;

          // o.position.z = -1;
          o.rotation.order = 'YXZ';
          o.scale.set(0.2, 0.2, 0.2);
          /* o.traverse(e => {
            e.castShadow = true;
          }); */

          /* o.quaternion.setFromUnitVectors(
            new THREE.Vector3(0, 0, -1),
            new THREE.Vector3(0, 0, 1)
          ); */
          o.updateMatrixWorld();
          // o.frustumCulled = false;
          for (let i = 0; i < o.children.length; i++) {
            o.children[i].frustumCulled = false;
          }

          model = o;

          scene.add(o);
          // scene.add(o.children[0]);
          // scene.add(o.children[0]);

        }, undefined, function ( e ) {

          console.error( e );

        } );
      }

      eyeMesh = (() => {
        const geometry = new THREE.BoxBufferGeometry(0.05, 0.05, 0.05);
        const material = new THREE.MeshPhongMaterial({
          color: 0x0000FF,
        });
        const mesh = new THREE.Mesh(geometry, material);
        mesh.visible = false;
        mesh.frustumCulled = false;
        return mesh;
      })();
      scene.add(eyeMesh);

      const localVector = new THREE.Vector3();
      const localVector2 = new THREE.Vector3();
      const localQuaternion = new THREE.Quaternion();
      const localMatrix = new THREE.Matrix4();
      const handMesh = (() => {
        const pointerGeometry = new THREE.CylinderBufferGeometry(0.001, 0.001, 1, 32, 1)
          .applyMatrix(localMatrix.makeTranslation(0, 1/2, 0))
          .applyMatrix(localMatrix.makeRotationFromQuaternion(
            localQuaternion.setFromUnitVectors(
              localVector.set(0, 1, 0),
              localVector2.set(0, 0, -1)
            )
          ));
        const boneGeometry = new THREE.BoxBufferGeometry(0.005, 0.005, 0.005);

        const geometry = new THREE.BufferGeometry();

        const positions = new Float32Array(boneGeometry.attributes.position.array.length * 2 * 6 * 4 + pointerGeometry.attributes.position.array.length * 2);
        const positionAttribute = new THREE.BufferAttribute(positions, 3);
        geometry.addAttribute('position', positionAttribute);

        const indices = new Uint16Array(boneGeometry.index.array.length * 2 * 6 * 4 + pointerGeometry.index.array.length * 2);
        const indexAttribute = new THREE.BufferAttribute(indices, 1);
        geometry.setIndex(indexAttribute);

        const material =  new THREE.MeshPhongMaterial({
          color: 0xFF0000,
        });

        const mesh = new THREE.Mesh(geometry, material);
        mesh.update = hands => {
          let positionIndex = 0;
          let indexIndex = 0;

          const _shiftIndex = array => {
            for (let l = 0; l < array.length; l++) {
              array[l] += positionIndex / 3;
            }
          };

          for (let i = 0; i < hands.length; i++) {
            const hand = hands[i];
            const {pointer, grip, wrist, fingers} = hand;
            const allFingers = [wrist].concat(fingers);
            for (let j = 0; j < allFingers.length; j++) {
              const bones = allFingers[j];
              for (let k = 0; k < bones.length; k++) {
                const positionFloat32Array = bones[k];
                if (positionFloat32Array) {
                  const position = localVector.fromArray(positionFloat32Array);
                  const newGeometry = boneGeometry.clone()
                    .applyMatrix(
                      localMatrix.makeTranslation(position.x, position.y, position.z)
                    );

                  _shiftIndex(newGeometry.index.array);

                  positions.set(newGeometry.attributes.position.array, positionIndex);
                  positionIndex += newGeometry.attributes.position.array.length;

                  indices.set(newGeometry.index.array, indexIndex);
                  indexIndex += newGeometry.index.array.length;
                }
              }
            }

            if (pointer) {
              const newGeometry = pointerGeometry.clone()
                .applyMatrix(
                  localMatrix.compose(
                    localVector.fromArray(pointer.position),
                    localQuaternion.fromArray(pointer.rotation),
                    localVector2.set(1, 1, 1),
                  )
                );

              _shiftIndex(newGeometry.index.array);

              positions.set(newGeometry.attributes.position.array, positionIndex);
              positionIndex += newGeometry.attributes.position.array.length;

              indices.set(newGeometry.index.array, indexIndex);
              indexIndex += newGeometry.index.array.length;
            }

            if (grip) {
              const newGeometry = pointerGeometry.clone()
                .applyMatrix(
                  localMatrix.compose(
                    localVector.fromArray(grip.position),
                    localQuaternion.fromArray(grip.rotation),
                    localVector2.set(1, 1, 1),
                  )
                );

              _shiftIndex(newGeometry.index.array);

              positions.set(newGeometry.attributes.position.array, positionIndex);
              positionIndex += newGeometry.attributes.position.array.length;

              indices.set(newGeometry.index.array, indexIndex);
              indexIndex += newGeometry.index.array.length;
            }
          }

          positionAttribute.needsUpdate = true;
          indexAttribute.needsUpdate = true;
          geometry.setDrawRange(0, indexIndex);
        };
        mesh.visible = false;
        mesh.frustumCulled = false;
        return mesh;
      })();
      scene.add(handMesh);
      const _onHands = hands => {
        handMesh.update(hands);
      };

      const terrainMeshes = [];
      const terrainMaterial = new THREE.MeshPhongMaterial({
        color: 0x666666,
      });
      const _getTerrainMesh = meshId => {
        let terrainMesh = terrainMeshes.find(terrainMesh => terrainMesh.meshId === meshId);
        if (!terrainMesh) {
          terrainMesh = _makeTerrainMesh(meshId);
          terrainMeshes.push(terrainMesh);
          scene.add(terrainMesh);
        }
        return terrainMesh;
      };
      const fakeArrayBuffer = new ArrayBuffer(3 * 4);
      const fakeFloat32Array = new Float32Array(fakeArrayBuffer, 0, 3);
      const fakeUint16Array = new Uint16Array(fakeArrayBuffer, 0, 3);
      const _makeTerrainMesh = meshId => {
        const geometry = new THREE.BufferGeometry();
        const gl = renderer.getContext();
        const attributes = renderer.getAttributes();

        geometry.addAttribute('position', new THREE.BufferAttribute(fakeFloat32Array, 3));
        attributes.update(geometry.attributes.position, gl.ARRAY_BUFFER);
        geometry.addAttribute('normal', new THREE.BufferAttribute(fakeFloat32Array, 3));
        attributes.update(geometry.attributes.normal, gl.ARRAY_BUFFER);
        geometry.setIndex(new THREE.BufferAttribute(fakeUint16Array, 1));
        attributes.update(geometry.index, gl.ELEMENT_ARRAY_BUFFER);

        const material = terrainMaterial;

        const mesh = new THREE.Mesh(geometry, material);
        mesh.matrixAutoUpdate = false;
        mesh.frustumCulled = false;
        mesh.meshId = meshId;
        return mesh;
      };
      const _loadTerrainMesh = (terrainMesh, {transformMatrix, positionBuffer, positionCount, normalBuffer, normalCount, indexBuffer, count}) => {
        terrainMesh.matrix.fromArray(transformMatrix);
        terrainMesh.matrixWorldNeedsUpdate = true;

        const {geometry} = terrainMesh;
        const attributes = renderer.getAttributes();

        attributes.get(geometry.attributes.position).buffer = positionBuffer;
        geometry.attributes.position.count = positionCount / 3;

        attributes.get(geometry.attributes.normal).buffer = normalBuffer;
        geometry.attributes.normal.count = normalCount / 3;

        attributes.get(geometry.index).buffer = indexBuffer;
        geometry.index.count = count / 1;
      };
      const _removeTerrainMesh = terrainMesh => {
        scene.remove(terrainMesh);
        terrainMesh.geometry.dispose();
      };
      const _clearTerrainMeshes = () => {
        for (let i = 0; i < terrainMeshes.length; i++) {
          _removeTerrainMesh(terrainMeshes[i]);
        }
        terrainMeshes.length = 0;
      };
      const _onMesh = updates => {
        for (let i = 0; i < updates.length; i++) {
          const update = updates[i];
          const {id, type} = update;

          if (type === 'new' || type === 'update') {
            _loadTerrainMesh(_getTerrainMesh(id), update);
          } else if (type === 'unchanged') {
            // nothing
          } else {
            const index = terrainMeshes.findIndex(terrainMesh => terrainMesh.meshId === id);
            if (index !== -1) {
              const terrainMesh = terrainMeshes[index];
              _removeTerrainMesh(terrainMesh);
              terrainMeshes.splice(index, 1);
            }
          }
        }
      };

      const planeMeshes = [];
      const planeGeometry = new THREE.PlaneBufferGeometry(1, 1);
      const planeMaterial = new THREE.MeshPhongMaterial({
        color: 0xFF0000,
      });
      const _loadPlanes = planes => {
        _clearPlanes();

        for (let i = 0; i < planes.length; i++) {
          const plane = planes[i];

          const mesh = new THREE.Mesh(planeGeometry, planeMaterial);
          mesh.frustumCulled = false;
          mesh.position.fromArray(plane.position);
          mesh.quaternion.fromArray(plane.rotation);
          mesh.scale.set(plane.size[0], plane.size[1], 1);
          mesh.updateMatrix();
          mesh.updateMatrixWorld();
          scene.add(mesh);
          planeMeshes.push(mesh);
        }
      };
      const _clearPlanes = () => {
        for (let i = 0; i < planeMeshes.length; i++) {
          scene.remove(planeMeshes[i]);
        }
        planeMeshes.length = 0;
      };
      const _onPlanes = planes => {
        _loadPlanes(planes);
      };

      let enabled = false;
      const _enable = () => {
        mesher = window.browser.magicleap.RequestMeshing();
        mesher.onmesh = _onMesh;
        planeTracker = window.browser.magicleap.RequestPlaneTracking();
        planeTracker.onplanes = _onPlanes;
        handTracker = window.browser.magicleap.RequestHandTracking();
        handTracker.onhands = _onHands;
        handMesh.visible = true;
        eyeTracker = window.browser.magicleap.RequestEyeTracking();

        enabled = true;
      };
      const _disable = () => {
        mesher.destroy();
        mesher = null;
        _clearTerrainMeshes();
        planeTracker.destroy();
        planeTracker = null;
        _clearPlanes();
        handTracker.destroy();
        handTracker = null;
        handMesh.visible = false;
        eyeTracker.destroy();
        eyeTracker = null;

        enabled = false;
      };
      if (window.browser && window.browser.magicleap) {
        _enable();
        window.addEventListener('keydown', e => {
          if (e.keyCode === 13) { // enter
            if (enabled) {
              _disable();
            } else {
              _enable();
            }
          }
        });
      }

      renderer = new THREE.WebGLRenderer({
        antialias: true,
        alpha: true,
      });
      renderer.setPixelRatio(window.devicePixelRatio);
      renderer.setSize(window.innerWidth, window.innerHeight);

      // window.browser.magicleap.RequestDepthPopulation(true);
      // renderer.autoClear = false;

      container.appendChild(renderer.domElement);

      renderer.setAnimationLoop(animate);
    }

    function animate(time, frame) {
      if (model) {
        const animationTime = 4000;
        const f = ((Date.now() % animationTime) / animationTime) * (Math.PI * 2);
        model.position.y = Math.sin(f) * 0.05;
        // model.rotation.x = Math.sin(f*4) * Math.PI*2*0.05;
        model.rotation.y = Math.sin(f*2) * Math.PI*2*0.05;
        model.rotation.z = Math.cos(f*2) * Math.PI*2*0.05;
        /* model.quaternion.setFromUnitVectors(
          new THREE.Vector3(0, 0, 1),
          new THREE.Vector3(Math.cos(f), 0, Math.sin(f)).normalize()
        ); */
        model.updateMatrixWorld();
      }
      if (eyeTracker) {
        const {fixation} = eyeTracker;
        const {position, rotation} = fixation;

        eyeMesh.position.fromArray(position);
        eyeMesh.quaternion.fromArray(rotation);
        eyeMesh.updateMatrix();
        eyeMesh.updateMatrixWorld();
        eyeMesh.visible = true;
      } else {
        eyeMesh.visible = false;
      }
      if (renderer.vr.enabled) {
        for (let i = 0; i < controllerMeshes.length; i++) {
          controllerMeshes[i].visible = false;
        }

        const inputSources = session.getInputSources();
        for (let i = 0; i < inputSources.length; i++) {
          const inputSource = inputSources[i];
          const pose = frame.getInputPose(inputSource);

          const controllerIndex = _getControllerIndex(inputSource);
          const controllerMesh = controllerMeshes[controllerIndex];
          controllerMesh.matrix.fromArray(pose.targetRay.transformMatrix);
          controllerMesh.updateMatrixWorld(true);
          controllerMesh.visible = true;
        }
      }

      renderer.render(scene, renderer.vr.enabled ? renderer.vr.getCamera(camera) : camera);
    }

    init();

    (async () => {
      console.log('request session');
      session = await navigator.xr.requestSession({
        exclusive: true,
      }).catch(err => Promise.resolve(null));

      if (session) {
        session.onselect = e => {
          const controllerIndex = e.inputSource.handedness === 'left' ? 0 : 1;
          const controllerMesh = controllerMeshes[controllerIndex];
          controllerMesh.matrixWorld.decompose(controllerMesh.position, controllerMesh.quaternion, controllerMesh.scale);

          const objectMesh = _makeControllerMesh(
            controllerMesh.position.x, controllerMesh.position.y, controllerMesh.position.z,
            controllerMesh.quaternion.x, controllerMesh.quaternion.y, controllerMesh.quaternion.z, controllerMesh.quaternion.w,
          );
          scene.add(objectMesh);
        };

        // console.log('request first frame');
        session.requestAnimationFrame((timestamp, frame) => {
          renderer.vr.setSession(session, {
            frameOfReferenceType: 'stage',
          });

          const {views} = frame.getViewerPose();
          const viewport = session.baseLayer.getViewport(views[0]);
          const width = viewport.width;
          const height = viewport.height;

          renderer.setSize(width * 2, height);

          renderer.setAnimationLoop(null);

          renderer.vr.enabled = true;
          renderer.vr.setAnimationLoop(animate);

          console.log('running!');
        });
      } else {
        console.log('no xr devices');
      }
    })();

    (() => {
      let cameraTracker = null;

      const server = window.browser.http.createServer((req, res) => {
        console.log('got request', req.url);

        let match;
        if (req.url === '/') {
          res.statusCode = 302;
          res.setHeader('Location', '/examples/camera_client.html');
          res.end();
        } else if (match = req.url.match(/\.(html|js)$/)) {
          fetch('file:///package' + req.url)
            .then(proxyRes => {
              if (proxyRes.ok) {
                return proxyRes.arrayBuffer()
                  .then(arrayBuffer => {
                    const type = (() => {
                      switch (match[1]) {
                        case 'html': return 'text/html';
                        case 'js': return 'application/javascript';
                        default: return 'text/plain';
                      }
                    })();
                    res.setHeader('Content-Type', type);

                    const buffer = Buffer.from(arrayBuffer);
                    res.end(buffer);
                  });
              } else {
                res.statusCode = proxyRes.status;
                res.end();
                return null;
              }
            })
            .catch(err => {
              console.warn(err.stack);
              res.statusCode = 500;
              res.end();
            });
        /* } else if (req.url === '/frame') {
          if (frameData) {
            console.log('send frame data', typeof frameData, frameData.constructor && frameData.constructor.name, frameData && frameData.length);
            res.setHeader('Content-Type', 'application/octet-stream');
            res.end(frameData);
          } else {
            res.statusCode = 404;
            res.end();
          } */
        } else {
          res.statusCode = 404;
          res.end();
        }
      });

      const wss = new window.browser.ws.Server({
        server,
      });
      wss.on('connection', (c, request) => {
        console.log('open connection');

        if (!cameraTracker) {
          const _onCameraFrame = e => {
            // console.log('camera frame', a.length, c.readyState === window.browser.ws.OPEN);
            const frameData = Buffer.from(e.data);

            for (let i = 0; i < cameraTracker.callbacks.length; i++) {
              cameraTracker.callbacks[i](frameData);
            }
          };
          window.browser.magicleap.RequestCamera(_onCameraFrame);

          cameraTracker = {
            callbacks: [],
          };
        }
        
        const _cameraTrackerCallback = frameData => {
          if (c.readyState === window.browser.ws.OPEN) {
            c.send(frameData);
          }
        };
        cameraTracker.callbacks.push(_cameraTrackerCallback);

        c.on('close', () => {
          console.log('close connection 1');
          cameraTracker.callbacks.splice(cameraTracker.callbacks.indexOf(_cameraTrackerCallback), 1);
          console.log('close connection 2');
          // window.browser.magicleap.CancelCamera(_onCameraFrame);
          console.log('close connection 3');
        });
      });

      server.listen(7999, '0.0.0.0', () => {
        console.log('listening');
      });
      server.on('error', err => {
        console.warn('server error', err.stack);
      });
    })();
  </script>
  </body>
</html>
